import { Game_Interpreter } from "@/objects/GameInterpreter.js";
import { Game_Vehicle } from "@/objects/GameVehicle.js";
import { Game_Event } from "@/objects/GameEvent.js";
import { Game_CommonEvent } from "@/objects/GameCommonEvent.js";

import { AudioManager } from "@/managers/AudioManager.js";

import { isZeroParallax } from "@/utils/ImageFileType.js";
import { clamp,mod} from "@/utils/number.js";
import { config } from "@/managers/ConfigManager.js";

import EventEmitter from "eventemitter3";

export class Game_Map extends EventEmitter {
    constructor() {
        super();
        this._interpreter = new Game_Interpreter();
        this._mapId = 0;
        this._tilesetId = 0;
        this._game_events = [];
        this._commonEvents = [];
        this._vehicles = [];
        
        this._displayX = 0;
        this._displayY = 0;
        this._displayXYLock=false;

        this._nameDisplay = true;
        this._scrollDirection = 2;
        this._scrollRest = 0;
        this._scrollSpeed = 4;
        this._parallaxName = '';
        this._parallaxZero = false;
        this._parallaxLoopX = false;
        this._parallaxLoopY = false;
        this._parallaxSx = 0;
        this._parallaxSy = 0;
        this._parallaxX = 0;
        this._parallaxY = 0;
        this._battleback1Name = null;
        this._battleback2Name = null;

        this._screenRow=Math.ceil(config.height/this.tileHeight());
        this._screenColumn=Math.ceil(config.width/this.tileWidth());
        this.createVehicles();
    }

    setup(mapId) {
        if (!$dataMap) {
            throw new Error('The map data is not available');
        }
        this._mapId = mapId;
        this._tilesetId = $dataMap.tilesetId;
        this._displayX = 0;
        this._displayY = 0;
        this.refereshVehicles();
        this.setupEvents();
        this.setupScroll();
        this.setupParallax();
        this.setupBattleback();
        this._needsRefresh = false;
    }

    parallaxOx() {
        if (this._parallaxZero) {
            return this._parallaxX * this.tileWidth();
        } else if (this._parallaxLoopX) {
            return this._parallaxX * this.tileWidth() / 2;
        } else {
            return 0;
        }
    }

    parallaxOy() {
        if (this._parallaxZero) {
            return this._parallaxY * this.tileHeight();
        } else if (this._parallaxLoopY) {
            return this._parallaxY * this.tileHeight() / 2;
        } else {
            return 0;
        }
    }

    tileset() {
        return $dataTilesets[this._tilesetId];
    }

    tilesetFlags() {
        let tileset = this.tileset();
        if (tileset) {
            return tileset.flags;
        } else {
            return [];
        }
    }

    displayName() {
        return $dataMap.displayName;
    }

    width() {
        return $dataMap.width;
    }

    height() {
        return $dataMap.height;
    }

    data() {
        return $dataMap.data;
    }

    tileWidth() {
        return 48;
    }

    tileHeight() {
        return 48;
    }

    mapId() {
        return this._mapId;
    }

    tilesetId() {
        return this._tilesetId;
    }

    displayX() {
        return this._displayX;
    }

    displayY() {
        return this._displayY;
    }

    parallaxName() {
        return this._parallaxName;
    }

    battleback1Name() {
        return this._battleback1Name;
    }

    battleback2Name() {
        return this._battleback2Name;
    }

    requestRefresh(mapId) {
        this._needsRefresh = true;
    }

    isNameDisplayEnabled() {
        return this._nameDisplay;
    }

    disableNameDisplay() {
        this._nameDisplay = false;
    }

    enableNameDisplay() {
        this._nameDisplay = true;
    }

    createVehicles() {
        this._vehicles[0] = new Game_Vehicle('boat');
        this._vehicles[1] = new Game_Vehicle('ship');
        this._vehicles[2] = new Game_Vehicle('airship');
    }

    refereshVehicles() {
        this._vehicles.forEach(function(vehicle) {
            vehicle.refresh();
        });
    }

    vehicles() {
        return this._vehicles;
    }

    vehicle(type) {
        if (type ==  0 || type == 'boat') {
            return this.boat();
        } else if (type ==  1 || type == 'ship') {
            return this.ship();
        } else if (type ==  2 || type == 'airship') {
            return this.airship();
        } else {
            return null;
        }
    }

    boat() {
        return this._vehicles[0];
    }

    ship() {
        return this._vehicles[1];
    }

    airship() {
        return this._vehicles[2];
    }

    events() {
        return this._game_events.filter(event=>!!event);
    }

    event(eventId) {
        return this._game_events[eventId];
    }

    eraseEvent(eventId) {
        this._game_events[eventId].erase();
    }

    parallelCommonEvents() {
        return $dataCommonEvents.filter(commonEvent=> commonEvent?.trigger == 2);
    }

    setupEvents() {
        this._game_events = [];
        for (let i = 0; i < $dataMap.events.length; i++) {
            if ($dataMap.events[i]) {
                this._game_events[i] = new Game_Event(this._mapId, i);
            }
        }
        this._commonEvents = this.parallelCommonEvents().map(function(commonEvent) {
            return new Game_CommonEvent(commonEvent.id);
        });
        this.refreshTileEvents();
    }

    setupScroll() {
        this._scrollDirection = 2;
        this._scrollRest = 0;
        this._scrollSpeed = 4;
    }

    setupParallax() {
        this._parallaxName = $dataMap.parallaxName || '';
        this._parallaxZero = isZeroParallax(this._parallaxName);
        this._parallaxLoopX = $dataMap.parallaxLoopX;
        this._parallaxLoopY = $dataMap.parallaxLoopY;
        this._parallaxSx = $dataMap.parallaxSx;
        this._parallaxSy = $dataMap.parallaxSy;
        this._parallaxX = 0;
        this._parallaxY = 0;
    }

    setupBattleback() {
        if ($dataMap.specifyBattleback) {
            this._battleback1Name = $dataMap.battleback1Name;
            this._battleback2Name = $dataMap.battleback2Name;
        } else {
            this._battleback1Name = null;
            this._battleback2Name = null;
        }
    }

    setParallaxXY(x,y){
        if(this._parallaxX!=x||this._parallaxY!=y){
            this._parallaxX=x;
            this._parallaxY=y;
            this.emit('map:parallax','position');
        }
    }

    setDisplayXY(x,y){
        if(this._displayX!=x||this._displayY!=y){
            this._displayX=x;
            this._displayY=y;
            if(!this._displayXYLock){
                this.emit('map:display','changed');
            }
        }
    }

    setDisplayPos(x, y) {
        let displayX=this._displayX;
        let displayY=this._displayY;
        let parallaxX=this._parallaxX;
        let parallaxY=this._parallaxY;

        if (this.isLoopHorizontal()) {
            displayX = mod(x,this.width());
            parallaxX = x;
        } else {
            let endX = this.width() - this.screenTileX();
            displayX = endX < 0 ? endX / 2 : clamp(x,0, endX);
            parallaxX = this._displayX;
        }
        if (this.isLoopVertical()) {
            displayY = mod(y,this.height());
            parallaxY = y;
        } else {
            let endY = this.height() - this.screenTileY();
            displayY = endY < 0 ? endY / 2 : clamp(y,0, endY);
            parallaxY = this._displayY;
        }

        this.setDisplayXY(displayX,displayY);
        this.setParallaxXY(parallaxX,parallaxY);
    }

    isEventRunning() {
        return this._interpreter.isRunning() || this.isAnyEventStarting();
    }

    isLoopHorizontal() {
        return $dataMap.scrollType == 2 || $dataMap.scrollType == 3;
    }

    isLoopVertical() {
        return $dataMap.scrollType == 1 || $dataMap.scrollType == 3;
    }

    isDashDisabled() {
        return $dataMap.disableDashing;
    }

    encounterList() {
        return $dataMap.encounterList;
    }

    encounterStep() {
        return $dataMap.encounterStep;
    }

    isOverworld() {
        return this.tileset()?.mode == 0;
    }

    screenTileX() {
        return this._screenColumn;
    }

    screenTileY() {
        return this._screenRow;
    }

    adjustX(x) {
        if (this.isLoopHorizontal() && x < this._displayX -
                (this.width() - this.screenTileX()) / 2) {
            return x - this._displayX + $dataMap.width;
        } else {
            return x - this._displayX;
        }
    }

    adjustY(y) {
        if (this.isLoopVertical() && y < this._displayY -
                (this.height() - this.screenTileY()) / 2) {
            return y - this._displayY + $dataMap.height;
        } else {
            return y - this._displayY;
        }
    }

    roundX(x) {
        return this.isLoopHorizontal() ? mod(x,this.width()) : x;
    }

    roundY(y) {
        return this.isLoopVertical() ? mod(y,this.height()) : y;
    }

    xWithDirection(x, d) {
        return x + (d == 6 ? 1 : d == 4 ? -1 : 0);
    }

    yWithDirection(y, d) {
        return y + (d == 2 ? 1 : d == 8 ? -1 : 0);
    }

    roundXWithDirection(x, d) {
        return this.roundX(x + (d == 6 ? 1 : d == 4 ? -1 : 0));
    }

    roundYWithDirection(y, d) {
        return this.roundY(y + (d == 2 ? 1 : d == 8 ? -1 : 0));
    }

    deltaX(x1, x2) {
        let result = x1 - x2;
        if (this.isLoopHorizontal() && Math.abs(result) > this.width() / 2) {
            if (result < 0) {
                result += this.width();
            } else {
                result -= this.width();
            }
        }
        return result;
    }

    deltaY(y1, y2) {
        let result = y1 - y2;
        if (this.isLoopVertical() && Math.abs(result) > this.height() / 2) {
            if (result < 0) {
                result += this.height();
            } else {
                result -= this.height();
            }
        }
        return result;
    }

    distance(x1, y1, x2, y2) {
        return Math.abs(this.deltaX(x1, x2)) + Math.abs(this.deltaY(y1, y2));
    }

    canvasToMapX(x) {
        let tileWidth = this.tileWidth();
        let originX = this._displayX * tileWidth;
        let mapX = Math.floor((originX + x) / tileWidth);
        return this.roundX(mapX);
    }

    canvasToMapY(y) {
        let tileHeight = this.tileHeight();
        let originY = this._displayY * tileHeight;
        let mapY = Math.floor((originY + y) / tileHeight);
        return this.roundY(mapY);
    }

    autoplay() {
        if ($dataMap.autoplayBgm) {
            if ($gamePlayer.isInVehicle()) {
                $gameSystem.saveWalkingBgm2();
            } else {
                AudioManager.playBgm($dataMap.bgm);
            }
        }
        if ($dataMap.autoplayBgs) {
            AudioManager.playBgs($dataMap.bgs);
        }
    }

    refreshIfNeeded() {
        if (this._needsRefresh) {
            this.refresh();
        }
    }

    refresh() {
        let events=this.events();
        this.tileEvents.length=0;
        for(let e of events){
            e.refresh();
            if(e.isTile()){
                this.tileEvents.push(e);
            }
        }
        for(let c of this._commonEvents){
            c.refresh();
        }
        this._needsRefresh = false;
    }

    refreshTileEvents() {
        this.tileEvents = this._game_events.filter(e=> e?.isTile());
    }

    eventsXy(x, y) {
        return this._game_events.filter(e=>e?.pos(x, y));
    }

    eventsXyNt(x, y) {
        return this._game_events.filter(e=>e?.posNt(x, y));
    }

    tileEventsXy(x, y) {
        return this.tileEvents.filter(e=>e?.posNt(x, y));
    }

    eventIdXy(x, y) {
        let list = this.eventsXy(x, y);
        return list.length == 0 ? 0 : list[0].eventId();
    }

    scrollDown(distance) {
        let parallaxY=this._parallaxY;
        let displayY=this._displayY;
        if (this.isLoopVertical()) {
            displayY=(displayY+distance)%$dataMap.width;
            if (this._parallaxLoopY) {
                parallaxY += distance;
            }
        } else if (this.height() >= this.screenTileY()) {
            let lastY = this._displayY;
            displayY = Math.min(displayY + distance,
                this.height() - this.screenTileY());
            parallaxY += displayY - lastY;
        }
        this.setDisplayXY(this._displayX,displayY);
        this.setParallaxXY(this._parallaxX,parallaxY);
    }

    scrollLeft(distance) {
        let parallaxX=this._parallaxX;
        let displayX=this._displayX;
        if (this.isLoopHorizontal()) {
            displayX = (displayX-distance)%$dataMap.width;
            if (this._parallaxLoopX) {
                parallaxX -= distance;
            }
        } else if (this.width() >= this.screenTileX()) {
            let lastX = displayX;
            displayX = Math.max(displayX - distance, 0);
            parallaxX += displayX - lastX;
        }
        this.setDisplayXY(displayX,this._displayY);
        this.setParallaxXY(parallaxX,this._parallaxY);
    }

    scrollRight(distance) {
        let parallaxX=this._parallaxX;
        let displayX=this._displayX;
        if (this.isLoopHorizontal()) {
            displayX = (displayX+distance)%$dataMap.width;
            if (this._parallaxLoopX) {
                parallaxX += distance;
            }
        } else if (this.width() >= this.screenTileX()) {
            let lastX = displayX;
            displayX = Math.min(displayX + distance,
                this.width() - this.screenTileX());
            parallaxX += displayX - lastX;
        }
        this.setDisplayXY(displayX,this._displayY);
        this.setParallaxXY(parallaxX,this._parallaxY);
    }

    scrollUp(distance) {
        let parallaxY=this._parallaxY;
        let displayY=this._displayY;
        if (this.isLoopVertical()) {
            displayY += (displayY+$dataMap.height - distance)%$dataMap.height;
            if (this._parallaxLoopY) {
                parallaxY -= distance;
            }
        } else if (this.height() >= this.screenTileY()) {
            let lastY = this._displayY;
            displayY = Math.max(displayY - distance, 0);
            parallaxY += displayY - lastY;
        }
        this.setDisplayXY(this._displayX,displayY);
        this.setParallaxXY(this._parallaxX,parallaxY);
    }

    isValid(x, y) {
        return x >= 0 && x < this.width() && y >= 0 && y < this.height();
    }

    checkPassage(x, y, bit) {
        let flags = this.tilesetFlags();
        let tiles = this.allTiles(x, y);
        for (let i = 0; i < tiles.length; i++) {
            let flag = flags[tiles[i]];
            if ((flag & 0x10) != 0)  // [*] No effect on passage
                continue;
            if ((flag & bit) == 0)   // [o] Passable
                return true;
            if ((flag & bit) == bit) // [x] Impassable
                return false;
        }
        return false;
    }

    tileId(x, y, z) {
        let width = $dataMap.width;
        let height = $dataMap.height;
        return $dataMap.data[(z * height + y) * width + x] || 0;
    }

    layeredTiles(x, y) {
        let tiles = [];
        for (let i = 0; i < 4; i++) {
            tiles.push(this.tileId(x, y, 3 - i));
        }
        return tiles;
    }

    allTiles(x, y) {
        let tiles = this.tileEventsXy(x, y).map(function(event) {
            return event.tileId();
        });
        return tiles.concat(this.layeredTiles(x, y));
    }

    autotileType(x, y, z) {
        let tileId = this.tileId(x, y, z);
        return tileId >= 2048 ? Math.floor((tileId - 2048) / 48) : -1;
    }

    isPassable(x, y, d) {
        return this.checkPassage(x, y, (1 << (d / 2 - 1)) & 0x0f);
    }

    isBoatPassable(x, y) {
        return this.checkPassage(x, y, 0x0200);
    }

    isShipPassable(x, y) {
        return this.checkPassage(x, y, 0x0400);
    }

    isAirshipLandOk(x, y) {
        return this.checkPassage(x, y, 0x0800) && this.checkPassage(x, y, 0x0f);
    }

    checkLayeredTilesFlags(x, y, bit) {
        let flags = this.tilesetFlags();
        return this.layeredTiles(x, y).some(function(tileId) {
            return (flags[tileId] & bit) != 0;
        });
    }

    isLadder(x, y) {
        return this.isValid(x, y) && this.checkLayeredTilesFlags(x, y, 0x20);
    }

    isBush(x, y) {
        return this.isValid(x, y) && this.checkLayeredTilesFlags(x, y, 0x40);
    }

    isCounter(x, y) {
        return this.isValid(x, y) && this.checkLayeredTilesFlags(x, y, 0x80);
    }

    isDamageFloor(x, y) {
        return this.isValid(x, y) && this.checkLayeredTilesFlags(x, y, 0x100);
    }

    terrainTag(x, y) {
        if (this.isValid(x, y)) {
            let flags = this.tilesetFlags();
            let tiles = this.layeredTiles(x, y);
            for (let i = 0; i < tiles.length; i++) {
                let tag = flags[tiles[i]] >> 12;
                if (tag > 0) {
                    return tag;
                }
            }
        }
        return 0;
    }

    regionId(x, y) {
        return this.isValid(x, y) ? this.tileId(x, y, 5) : 0;
    }

    startScroll(direction, distance, speed) {
        this._scrollDirection = direction;
        this._scrollRest = distance;
        this._scrollSpeed = speed;
    }

    isScrolling() {
        return this._scrollRest > 0;
    }

    update(sceneActive) {
        this.refreshIfNeeded();
        if (sceneActive) {
            this.updateInterpreter();
        }
        this.updateScroll();
        this.updateEvents();
        this.updateVehicles();
        this.updateParallax();
    }

    updateScroll() {
        if (this.isScrolling()) {
            let lastX = this._displayX;
            let lastY = this._displayY;
            this.doScroll(this._scrollDirection, this.scrollDistance());
            if (this._displayX == lastX && this._displayY == lastY) {
                this._scrollRest = 0;
            } else {
                this._scrollRest -= this.scrollDistance();
            }
        }
    }

    scrollDistance() {
        return Math.pow(2, this._scrollSpeed-8);
    }

    doScroll(direction, distance) {
        switch (direction) {
        case 2:
            this.scrollDown(distance);
            break;
        case 4:
            this.scrollLeft(distance);
            break;
        case 6:
            this.scrollRight(distance);
            break;
        case 8:
            this.scrollUp(distance);
            break;
        }
    }

    updateEvents() {
        for(let e of this._game_events){
            e?.update();
        }
        
        for(let c of this._commonEvents){
            c.update();
        }
    }

    updateVehicles() {
        for(let v of this._vehicles){
            v.update();
        }
    }

    updateParallax() {
        let parallaxX=this._parallaxX;
        let parallaxY=this._parallaxY;
        if (this._parallaxLoopX) {
            parallaxX += this._parallaxSx / this.tileWidth() / 2;
        }
        if (this._parallaxLoopY) {
            parallaxY += this._parallaxSy / this.tileHeight() / 2;
        }
        this.setParallaxXY(parallaxX,parallaxY);
    }

    changeTileset(tilesetId) {
        if(this._tilesetId!=tilesetId){
            this._tilesetId = tilesetId;
            this.emit('map:tileset','changed');
            this.refresh();
        }
    }

    changeBattleback(battleback1Name, battleback2Name) {
        this._battleback1Name = battleback1Name;
        this._battleback2Name = battleback2Name;
        this.emit('map:battleback','changed');
    }

    changeParallax(name, loopX, loopY, sx, sy) {
        if(this._parallaxName!=name){
            this._parallaxName = name;
            this._parallaxZero = isZeroParallax(this._parallaxName);
            if (this._parallaxLoopX && !loopX) {
                this._parallaxX = 0;
            }
            if (this._parallaxLoopY && !loopY) {
                this._parallaxY = 0;
            }
            this._parallaxLoopX = loopX;
            this._parallaxLoopY = loopY;
            this._parallaxSx = sx;
            this._parallaxSy = sy;
            this.emit('map:parallax','name');
        }
    }

    updateInterpreter() {
        for (;;) {
            this._interpreter.update();
            if (this._interpreter.isRunning()) {
                return;
            }
            if (this._interpreter.eventId() > 0) {
                this.unlockEvent(this._interpreter.eventId());
                this._interpreter.clear();
            }
            if (!this.setupStartingEvent()) {
                return;
            }
        }
    }

    unlockEvent(eventId) {
        this._game_events[eventId]?.unlock();
    }

    setupStartingEvent() {
        this.refreshIfNeeded();
        if (this._interpreter.setupReservedCommonEvent()) {
            return true;
        }
        // if (this.setupTestEvent()) {
        //     return true;
        // }
        if (this.setupStartingMapEvent()) {
            return true;
        }
        if (this.setupAutorunCommonEvent()) {
            return true;
        }
        return false;
    }

    setupTestEvent() {
        if (window.$testEvent) {
            this._interpreter.setup($testEvent, 0);
            $testEvent = null;
            return true;
        }
        return false;
    }

    setupStartingMapEvent() {
        for (let event of this._game_events) {
            if (event?.isStarting()) {
                event.clearStartingFlag();
                this._interpreter.setup(event.list(), event.eventId());
                return true;
            }
        }
        return false;
    }

    setupAutorunCommonEvent() {
        for (let i = 0; i < $dataCommonEvents.length; i++) {
            let event = $dataCommonEvents[i];
            if (event && event.trigger == 1 && $gameSwitches.value(event.switchId)) {
                this._interpreter.setup(event.list);
                return true;
            }
        }
        return false;
    }

    isAnyEventStarting() {
        return this._game_events.some(e=> e?.isStarting());
    }

    serialize(){
        return {
            interpreter:this._interpreter.serialize(),
            mapId: this._mapId,
            tilesetId: this._tilesetId,
            events: this._game_events.map(e=>e.serialize()),
            commonEvents: this._commonEvents.map(c=>c.serialize()),
            vehicles: this._vehicles.map(v=>v.serialize()),
            displayX: this._displayX,
            displayY: this._displayY,
            nameDisplay: this._nameDisplay,
            scrollDirection: this._scrollDirection,
            scrollRest: this._scrollRest,
            scrollSpeed: this._scrollSpeed,
            parallaxName: this._parallaxName,
            parallaxZero: this._parallaxZero,
            parallaxLoopX: this._parallaxLoopX,
            parallaxLoopY: this._parallaxLoopY,
            parallaxSx: this._parallaxSx,
            parallaxSy: this._parallaxSy,
            parallaxX: this._parallaxX,
            parallaxY: this._parallaxY,
            battleback1Name: this._battleback1Name,
            battleback2Name: this._battleback2Name
        }
    }

    deserialize(data){
        this._interpreter.deserialize(data.interpreter);
        this._mapId = data.mapId;
        this._tilesetId = data.tilesetId;
        this._game_events = data.events.map(e =>{
            let event=new Game_Event();
            event.deserialize(e);
            return event;
        });
        this._commonEvents = data.commonEvents.map(e =>{
            let event=new Game_CommonEvent();
            event.deserialize(e);
            return event;
        });
        this._vehicles = data.vehicles.map(v =>{
            let event=new Game_Vehicle();
            event.deserialize(v);
            return event;
        });
        this._displayX = data.displayX;
        this._displayY = data.displayY;
        this._nameDisplay = data.nameDisplay;
        this._scrollDirection = data.scrollDirection;
        this._scrollRest = data.scrollRest;
        this._scrollSpeed = data.scrollSpeed;
        this._parallaxName = data.parallaxName;
        this._parallaxZero = data.parallaxZero;
        this._parallaxLoopX = data.parallaxLoopX;
        this._parallaxLoopY = data.parallaxLoopY;
        this._parallaxSx = data.parallaxSx;
        this._parallaxSy = data.parallaxSy;
        this._parallaxX = data.parallaxX;
        this._parallaxY = data.parallaxY;
        this._battleback1Name = data.battleback1Name;
        this._battleback2Name = data.battleback2Name;
    }
}